﻿using System;
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace XamlX.TypeSystem;

// Yet another Cecil member that was never exposed nor used outside of WinMD code
// https://github.com/jbevain/cecil/blob/56d4409b8a0165830565c6e3f96f41bead2c418b/Mono.Cecil/GenericParameterResolver.cs#L5

internal sealed class GenericParameterResolver
{
	internal static TypeReference ResolveReturnTypeIfNeeded(MethodReference methodReference)
	{
		if (methodReference.DeclaringType.IsArray && methodReference.Name == "Get")
			return methodReference.ReturnType;

		var genericInstanceMethod = methodReference as GenericInstanceMethod;
		var declaringGenericInstanceType = methodReference.DeclaringType as GenericInstanceType;

		if (genericInstanceMethod == null && declaringGenericInstanceType == null)
			return methodReference.ReturnType;

		return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, methodReference.ReturnType);
	}

	internal static TypeReference ResolveFieldTypeIfNeeded(FieldReference fieldReference)
	{
		return ResolveIfNeeded(null, fieldReference.DeclaringType as GenericInstanceType, fieldReference.FieldType);
	}

	internal static TypeReference ResolvePropertyTypeIfNeeded(PropertyReference propertyReference)
	{
		return ResolveIfNeeded(null, propertyReference.DeclaringType as GenericInstanceType, propertyReference.PropertyType);
	}

	internal static TypeReference ResolveParameterTypeIfNeeded(MethodReference method, ParameterReference parameter)
	{
		var genericInstanceMethod = method as GenericInstanceMethod;
		var declaringGenericInstanceType = method.DeclaringType as GenericInstanceType;

		if (genericInstanceMethod == null && declaringGenericInstanceType == null)
			return parameter.ParameterType;

		return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, parameter.ParameterType);
	}

	internal static TypeReference ResolveParameterTypeIfNeeded(PropertyReference property, ParameterReference parameter)
	{
		var declaringGenericInstanceType = property.DeclaringType as GenericInstanceType;

		if (declaringGenericInstanceType == null)
			return parameter.ParameterType;

		return ResolveIfNeeded(null, declaringGenericInstanceType, parameter.ParameterType);
	}

	internal static TypeReference ResolveVariableTypeIfNeeded(MethodReference method, VariableReference variable)
	{
		var genericInstanceMethod = method as GenericInstanceMethod;
		var declaringGenericInstanceType = method.DeclaringType as GenericInstanceType;

		if (genericInstanceMethod == null && declaringGenericInstanceType == null)
			return variable.VariableType;

		return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, variable.VariableType);
	}

	private static TypeReference ResolveIfNeeded(IGenericInstance? genericInstanceMethod,
		IGenericInstance? declaringGenericInstanceType, TypeReference parameterType)
	{
		if (genericInstanceMethod is null && declaringGenericInstanceType is null)
			return parameterType;

		var byRefType = parameterType as ByReferenceType;
		if (byRefType != null)
			return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, byRefType);

		var arrayType = parameterType as ArrayType;
		if (arrayType != null)
			return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, arrayType);

		var genericInstanceType = parameterType as GenericInstanceType;
		if (genericInstanceType != null)
			return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, genericInstanceType);

		var genericParameter = parameterType as GenericParameter;
		if (genericParameter != null)
			return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, genericParameter);

		var requiredModifierType = parameterType as RequiredModifierType;
		if (requiredModifierType != null && ContainsGenericParameters(requiredModifierType))
			return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType,
				requiredModifierType.ElementType);

		if (ContainsGenericParameters(parameterType))
			throw new Exception("Unexpected generic parameter.");

		return parameterType;
	}

	private static TypeReference ResolveIfNeeded(IGenericInstance? genericInstanceMethod,
		IGenericInstance? genericInstanceType, GenericParameter genericParameterElement)
	{
		return (genericParameterElement.MetadataType == MetadataType.MVar)
			? (genericInstanceMethod != null
				? genericInstanceMethod.GenericArguments[genericParameterElement.Position]
				: genericParameterElement)
			: genericInstanceType!.GenericArguments[genericParameterElement.Position];
	}

	private static ArrayType ResolveIfNeeded(IGenericInstance? genericInstanceMethod,
		IGenericInstance? genericInstanceType, ArrayType arrayType)
	{
		return new ArrayType(ResolveIfNeeded(genericInstanceMethod, genericInstanceType, arrayType.ElementType),
			arrayType.Rank);
	}

	private static ByReferenceType ResolveIfNeeded(IGenericInstance? genericInstanceMethod,
		IGenericInstance? genericInstanceType, ByReferenceType byReferenceType)
	{
		return new ByReferenceType(ResolveIfNeeded(genericInstanceMethod, genericInstanceType,
			byReferenceType.ElementType));
	}

	private static GenericInstanceType ResolveIfNeeded(IGenericInstance? genericInstanceMethod,
		IGenericInstance? genericInstanceType, GenericInstanceType genericInstanceType1)
	{
		if (!ContainsGenericParameters(genericInstanceType1))
			return genericInstanceType1;

		var newGenericInstance = new GenericInstanceType(genericInstanceType1.ElementType);

		foreach (var genericArgument in genericInstanceType1.GenericArguments)
		{
			if (!genericArgument.IsGenericParameter)
			{
				newGenericInstance.GenericArguments.Add(ResolveIfNeeded(genericInstanceMethod, genericInstanceType,
					genericArgument));
				continue;
			}

			var genParam = (GenericParameter)genericArgument;

			switch (genParam.Type)
			{
				case GenericParameterType.Type:
				{
					if (genericInstanceType == null)
						throw new NotSupportedException();

					newGenericInstance.GenericArguments.Add(genericInstanceType.GenericArguments[genParam.Position]);
				}
					break;

				case GenericParameterType.Method:
				{
					if (genericInstanceMethod == null)
						newGenericInstance.GenericArguments.Add(genParam);
					else
						newGenericInstance.GenericArguments.Add(
							genericInstanceMethod.GenericArguments[genParam.Position]);
				}
					break;
			}
		}

		return newGenericInstance;
	}

	private static bool ContainsGenericParameters(TypeReference? typeReference)
	{
		var genericParameter = typeReference as GenericParameter;
		if (genericParameter != null)
			return true;

		var arrayType = typeReference as ArrayType;
		if (arrayType != null)
			return ContainsGenericParameters(arrayType.ElementType);

		var pointerType = typeReference as PointerType;
		if (pointerType != null)
			return ContainsGenericParameters(pointerType.ElementType);

		var byRefType = typeReference as ByReferenceType;
		if (byRefType != null)
			return ContainsGenericParameters(byRefType.ElementType);

		var sentinelType = typeReference as SentinelType;
		if (sentinelType != null)
			return ContainsGenericParameters(sentinelType.ElementType);

		var pinnedType = typeReference as PinnedType;
		if (pinnedType != null)
			return ContainsGenericParameters(pinnedType.ElementType);

		var requiredModifierType = typeReference as RequiredModifierType;
		if (requiredModifierType != null)
			return ContainsGenericParameters(requiredModifierType.ElementType);

		var genericInstance = typeReference as GenericInstanceType;
		if (genericInstance != null)
		{
			foreach (var genericArgument in genericInstance.GenericArguments)
			{
				if (ContainsGenericParameters(genericArgument))
					return true;
			}

			return false;
		}

		if (typeReference is TypeSpecification)
			throw new NotSupportedException();

		return false;
	}
}